// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.map;

import bats.Assets;
import bats.SpatialProperties;
import bats.scene.SceneControl;
import bats.scene.SceneNode;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import java.io.PrintStream;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.xml.XmlOutput;

/**
 * A scene control to manage height-mapped terrain in a world map.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class TerrainControl
        extends SceneControl {
    // *************************************************************************
    // constants

    /**
     * if true, apply wire frame material to terrain
     */
    final private static boolean terrainDebug = false;
    /**
     * maximum radius for shadows (in meters, &gt;0)
     */
    final private static float maxRadiusForShadows = 100f;
    /**
     * size of terrain patches (samples on a side)
     */
    final private static int patchSize = 33;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(TerrainControl.class.getName());
    /**
     * name for the TerrainQuad node
     */
    final private static String nodeName = "terrain";
    /**
     * tag for ground planes in XML files
     */
    final public static String xmlTag = "terrain";
    // *************************************************************************
    // constructors

    /**
     * Instantiate the enabled control for a specified scene, asset, and range
     * of elevations.
     *
     * @param scene which scene will contain this terrain (not null)
     * @param topography name of the topography asset (not null)
     * @param baseY lowest possible y-coordinate (world units)
     * @param topY highest actual y-coordinate (world units, &ge;baseY)
     */
    public TerrainControl(SceneNode scene, String topography, float baseY,
            float topY) {
        super(scene);
        assert topography != null;
        assert topY >= baseY : baseY;
        load(topography, baseY, topY);

        assert isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Reload this terrain from assets.
     */
    public void reload() {
        String topography = SpatialProperties.getTopography(spatial);
        float baseY = getBaseY();
        float topY = SpatialProperties.getTopY(spatial);
        delete();
        load(topography, baseY, topY);
    }

    /**
     * Serialize this terrain to an XML stream.
     *
     * @param stream output stream (not null)
     * @param indent (not null)
     */
    public void writeXml(PrintStream stream, String indent) {
        assert indent != null;

        stream.printf("%s<%s", indent, xmlTag);

        float baseY = getBaseY();
        baseY *= scene.getScaleFactor(); // convert world units to meters
        XmlOutput.putAttribute(stream, "baseY", baseY, 4);

        float topY = SpatialProperties.getTopY(spatial);
        topY *= scene.getScaleFactor(); // convert world units to meters
        XmlOutput.putAttribute(stream, "topY", topY, 4);

        String topography = SpatialProperties.getTopography(spatial);
        XmlOutput.putAttribute(stream, "topography", topography);

        stream.println("/>");
    }
    // *************************************************************************
    // private methods

    /**
     * Get the lowest possible Y-coordinate for this terrain.
     *
     * @return world Y-coordinate (in meters)
     */
    private float getBaseY() {
        float result = MySpatial.getWorldLocation(spatial).y;
        return result;
    }

    /**
     * Create the spatial from parameters.
     *
     * @param topography name of the topography asset (not null)
     * @param baseY lowest possible Y-coordinate
     * @param topY highest actual Y-coordinate (&ge;baseY)
     */
    private void load(String topography, float baseY, float topY) {
        assert topY >= baseY : baseY;
        /*
         * Create the node.
         */
        AbstractHeightMap heightMap = Assets.loadHeightMap(topography);
        int mapSize = heightMap.getSize() + 1; // number of samples on a side
        float[] heightArray = heightMap.getHeightMap();
        TerrainQuad quad =
                new TerrainQuad(nodeName, patchSize, mapSize, heightArray);
        quad.addControl(this);
        /*
         * Add the level-of-detail (LOD) control.
         */
        Camera camera = scene.getApplication().getCamera();
        TerrainLodControl control = new TerrainLodControl(quad, camera);
        float lodMultiplier = 1f;
        DistanceLodCalculator calculator =
                new DistanceLodCalculator(patchSize, lodMultiplier);
        control.setLodCalculator(calculator);
        spatial.addControl(control);
        /*
         * Scale and position the node.
         */
        float[] minMaxHeights = heightMap.findMinMaxHeights();
        float maxHeight = minMaxHeights[1];
        assert maxHeight >= 0f : maxHeight;
        float yScale = (topY - baseY) / maxHeight; // world units
        MapNode map = scene.getWorld().getMap();
        float radius = map.getRadius(); // meters
        float xzScale = (2f * radius) / mapSize; // meters per pixel
        float scaleFactor = scene.getScaleFactor();
        xzScale /= scaleFactor; // world units per pixel
        Vector3f scale = new Vector3f(xzScale, yScale, xzScale);
        spatial.setLocalScale(scale);

        Vector3f center = new Vector3f(0f, baseY, 0f);
        MySpatial.setWorldLocation(spatial, center);

        map.attach(spatial);
        /*
         * Apply a material.
         */
        Material material;
        if (terrainDebug) {
            material = Assets.createWireframeMaterial(ColorRGBA.Green);
        } else {
            float textureScale = 15f;
            material = Assets.createTerrainMaterial("tex2", textureScale,
                    "dirt", "dirt", "dirt");
        }
        spatial.setMaterial(material);
        /*
         * Add a rigid body control.
         */
        float mass = 0f; // immobile
        RigidBodyControl rigidBodyControl = new RigidBodyControl(mass);
        spatial.addControl(rigidBodyControl);
        scene.getWorld().getPhysics().getSpace().addAll(spatial);
        rigidBodyControl.setFriction(MapNode.friction);
        /*
         * Set the other properties of the terrain.
         */
        RenderQueue.ShadowMode shadowMode;
        if (radius <= maxRadiusForShadows) {
            shadowMode = RenderQueue.ShadowMode.CastAndReceive;
        } else {
            shadowMode = RenderQueue.ShadowMode.Off;
        }
        spatial.setShadowMode(shadowMode);
        SpatialProperties.setHardness(spatial, "dirt");
        SpatialProperties.setTopography(spatial, topography);
        SpatialProperties.setTopY(spatial, topY);
    }
}